Skip to content

feat(nixos): NixOS module with systemd hardening#3

Merged
jeanlucthumm merged 23 commits intomainfrom
feat/nixos-module
Feb 9, 2026
Merged

feat(nixos): NixOS module with systemd hardening#3
jeanlucthumm merged 23 commits intomainfrom
feat/nixos-module

Conversation

@jeanlucthumm
Copy link
Owner

Summary

Adds a NixOS module (nixosModules.openclaw) that runs the gateway as an isolated system user with systemd hardening. Contains the blast radius if the LLM is compromised.

  • Dedicated openclaw system user with minimal privileges
  • Systemd hardening: ProtectHome, PrivateTmp, NoNewPrivileges, syscall filtering, namespace isolation
  • Multi-instance support via instances.<name>
  • Credential loading from files at runtime (secrets never touch the Nix store)
  • Documents and skills installation support
  • VM integration test
  • Auth configuration with assertions

Context

Based on upstream openclaw#24.

Add system service module that runs the gateway as an isolated system
user with systemd hardening (ProtectHome, PrivateTmp, NoNewPrivileges,
etc.). Exports nixosModules.openclaw from the flake.

Based on work by @das-monki.
Tests:
- Service starts successfully
- User/group created (clawdbot:clawdbot)
- State directories exist with correct ownership
- Config file symlinked to /var/lib/clawdbot
- Hardening: ProtectHome hides /home from service
- Service runs as clawdbot user

Run with: nix build .#checks.x86_64-linux.nixos-module -L
Interactive: nix build .#checks.x86_64-linux.nixos-module.driverInteractive
The /run/agenix and /run/secrets paths were causing service startup
failure when they don't exist. With ProtectSystem=strict, /run/* is
already readable, so these explicit paths are unnecessary.
Adds providers.anthropic.oauthCredentialsDir option that bind-mounts
the user's ~/.claude directory into the service's sandbox. This allows
using Claude Pro/Max subscription via OAuth while maintaining isolation.

The service:
- Still has ProtectHome=true (can't see /home generally)
- Gets read-write access to ONLY the specified .claude dir via BindPaths
- Can refresh OAuth tokens as needed

Example:
  services.clawdbot.providers.anthropic.oauthCredentialsDir = "/home/user/.claude";
Adds second test node (oauth) that verifies:
- Service starts with oauthCredentialsDir configured
- Credentials are accessible via bind-mount at /var/lib/clawdbot/.claude
- Other files in /home remain inaccessible (ProtectHome still works)
The gateway uses os.networkInterfaces() which requires AF_NETLINK
sockets to enumerate interfaces. Without this, the service fails with:
  uv_interface_addresses returned Unknown system error 97
System services aren't exposed externally, so disable gateway auth by
default. Users can override via configOverrides if needed.
Match home-manager behavior - don't set gateway.auth.mode in the module.
Users can configure via configOverrides based on their use case.
Upstream now requires gateway authentication by default (c4a80f4ed).
Add gateway.auth.{mode,tokenFile,passwordFile} options to support this.

Users can either:
- Set tokenFile/passwordFile to load credentials from files at runtime
- Use configOverrides to set gateway.auth.token directly in the config

The wrapper script loads credentials from files and sets the appropriate
environment variables (CLAWDBOT_GATEWAY_TOKEN/CLAWDBOT_GATEWAY_PASSWORD).
The @mariozechner/clipboard native module (transitive dependency from pi-tui)
needs resource syscalls that were blocked by ~@resources filter, causing
SIGSYS crashes on headless systems.
Remove ~@PRIVILEGED and ~@resources filters - Node.js with native modules
needs these syscalls. Security is maintained through:
- CapabilityBoundingSet = "" (no capabilities)
- NoNewPrivileges = true
- ProtectHome, ProtectSystem = strict
- RestrictNamespaces, PrivateDevices, PrivateTmp
Instance-level providers.telegram and providers.anthropic options now
default to the top-level cfg values instead of hardcoded defaults.
This ensures that when users set top-level providers and only override
specific instance options (like configOverrides), the provider settings
are still inherited.
BindPaths can't access /home when ProtectHome=true, so disable it
when oauthCredentialsDir is configured (typically points to ~/.claude).
The short-lived OAuth tokens from Claude CLI's .credentials.json aren't
practical for server deployments. Replace with oauthTokenFile which loads
a long-lived token from `claude setup-token`.

- Remove oauthCredentialsDir option and bind mount logic
- Add oauthTokenFile option (sets ANTHROPIC_OAUTH_TOKEN env var)
- Restore ProtectHome=true now that we don't need /home access
- Update module header with new example usage
The OAuth bind-mount test referenced oauthCredentialsDir which was
removed in ddc7bd5. The new oauthTokenFile approach doesn't need
bind mounts - tokens are loaded via env vars from paths outside /home.
These options were defined but never wired up to the config generation.
Rather than ship dead options, remove them and add a note pointing to
the home-manager module which has the full implementation.
Add documents-skills.nix with parallel implementation to home-manager:
- Documents: copies AGENTS.md, SOUL.md, TOOLS.md to workspace with
  appended Nix-managed tools report
- Skills: supports copy and inline modes (symlink omitted for system
  service where it doesn't make sense)
- Uses systemd-tmpfiles for installation

The implementation is kept separate to ease future consolidation with
the home-manager module.
The `config` option (using generatedConfigOptions from upstream schema)
was defined but never wired into config generation. Users setting
`inst.config.*` would see no effect, which is confusing.

The `configOverrides` option remains as the escape hatch for arbitrary
JSON config. If typed schema options are needed later, they can be
added with actual implementation.
Fail early with clear messages when:
- Neither apiKeyFile nor oauthTokenFile is set for Anthropic
- gateway.auth.tokenFile is missing when mode is "token"
- gateway.auth.passwordFile is missing when mode is "password"

Update test to provide dummy tokens to satisfy assertions.
The instances option was defined in both options.nix and clawdbot.nix.
The one in clawdbot.nix always overrode the one in options.nix, making
the latter dead code.
@jeanlucthumm jeanlucthumm merged commit ba628e4 into main Feb 9, 2026
3 checks passed
@jeanlucthumm jeanlucthumm deleted the feat/nixos-module branch February 9, 2026 03:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant